package net.wanderinghorse.jsonmessage;

import net.wanderinghorse.json.JSONObject;

import java.io.Serializable;
import java.util.Iterator;

/**
 * Don't use this - it is an obsolete(?) artifact. It will be removed from the API
 * unless a use is found for it.
 * 
 * A classloader-like Singleton type for dynamically loading Response classes,
 * for purposes of polymorphic dispatching of incoming requests to their proper
 * Response handler.
 *
 */
public class ResponseLoader implements Serializable {
    private static final long serialVersionUID = 1L;

    private ResponseLoader()
    {}

    /**
     * Tries to load JSONMessage.getConfig() and process any
     * embedded configuration options for this class.
     *
     * Specifically, it looks for:
     *
     * <pre>
     * { ...
     *  "ResponseLoader":{
     *    "aliases"{ "alias1":"class.name.one", ...}
     *  }
     * }
     * </pre>
     *
     * Any "aliases" entries are mapped by passing them to alias().
     * Results are undefined if any aliases entries are not strings.
     * Any JSON exceptions are silently ignored, and not propagated.
     * If the event of an exception, the aliases list might be only 
     * partially populated.
     */
    private void loadProperties()
    {
        JSONObject jo;
        try {
            jo = JSONMessage.getConfig();
        }
        catch( Exception ex ) {
            return;
        }
        try {
            jo = jo.optJSONObject("ResponseLoader");
            if( null == jo ) return;
            jo = jo.optJSONObject( "aliases");
            if( null == jo ) return;
            /** FIXME: collect the aliases into a map first, then
                insert them after we are past any possible
                JSONException-throwing bits. That way we can
                guaranty that if any of the list is aliased, all
                of it is aliased. A partially-populated list
                could cause us Grief downstream.
             */
            Iterator<Object> it = jo.sortedKeys();
            while( it.hasNext() ) {
                final String key = it.next().toString();
                final String val = jo.optString(key);
                if( null != val ) {
                    _instance.alias( key, val );
                }
            }
        }
        catch( Exception ex ) {
            return;
        }
    }

    /**
     * Internal flag specifying whether or not the instance() object
     * has already read (or tried to read) the shared config data.
     * It only attempts this one time, the first time instance()
     * is called.
     */
    private boolean hasRunInit = false;
    /**
     * The shared ResponseLoader singleton.
     */
    private static final ResponseLoader _instance = new ResponseLoader();
    /**
     * The public interface for getting a new instance of this class.
     *
     * @return Shared instance of this class.
     */
    public static ResponseLoader instance()
    {
        synchronized (_instance) {
            if( ! _instance.hasRunInit ) {
                _instance.hasRunInit = true;
                _instance.loadProperties();
            }
        }
        return _instance;
    }

    /**
     * Overridden to always return the instance() object.
     *
     * @return The same value as instance().
     * @throws CloneNotSupportedException The clone() interface requires
     * this, but this function never throws.
     */
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return instance();
    }

    /**
     * Internal helper to map aliases to class names for purposes of
     * loadRequestHandler().
     */
    private final Aliaser ali = new Aliaser();

    /**
     * Aliases k as the equivalent of v for purposes of loadRequestHandler()
     * lookups.
     * @param alias_ The alias.
     * @param realName The class name (or other alias) to alias to.
     * @throws IllegalArgumentException if either value is null or
     * the values are equal.
     */
    public void alias( final String alias_, final String realName) {
        if( (null == alias_) || (null== realName) || (alias_.equals(realName)) ) {
            throw new IllegalArgumentException("ResponseLoader.alias(): neither value may be null "+
                    "and they may not be equal.");
        }
        this.ali.alias(alias_, realName);
    }


    /**
     * Convenience form of alias( n, c.getName() ).
     *
     * @param alias_ Alias for c.getName().
     * @param c Class to pull name from (using c.getName()).
     * @throws IllegalArgumentException See alias(String,String) for details.
     */
    public void alias( final String alias_, final Class<? extends Response> c )
    {
        this.alias( alias_, (null==c) ? null : c.getName() );
    }

    /**
     * Tries to loadRequestHandler a new Response object by using the given key as a
     * Response class name. Aliases defined via alias() are resolved.
     *
     * As a special case, if no class is found for the given key, a key name of
     * ("net.wanderinghorse.jsonmessage.response."+key) is also tried. 
     *
     * @param key The lookup key, as set via map().
     * @param convertExceptionsToErrorResponse If true then most exceptions thrown
     * while loading the Response class (e.g. ClassCastException) are converted
     * to ErrorResponse objects, otherwise an exception in those bits causes
     * null to be returned. The exception (as it were) to this rule is that a
     * ClassNotFoundException is treated as a null return value.
     * @return The new instance, or null if no mapping is found. If convertExceptionsToErrorResponse
     * is true and instantiation throws then that exception is converted to a Response
     * object which encapsulates the error information.
     * @throws RuntimeException If too many levels of alias expansion are encountered.
     * This particular check ignores the convertExceptionsToErrorResponse option.
     */
    Response loadResponse( String key, boolean convertExceptionsToErrorResponse )
    {
        if( null == key ) return null;
        final String x = this.ali.resolve(key);
        if( null != x ) key = x;

        try {
            ClassLoader cl =
                    Thread.currentThread().getContextClassLoader()
                    //ClassLoader.getSystemClassLoader()// works from IntelliJ but not ant!
                    ;
            if( null == cl /* is this possible? The API docs are not specific. */) return null;
            Class c = null;
//            c = cl.loadClass(key);
            try { c = cl.loadClass(key); }
            catch( ClassNotFoundException cnf ) {
                key = "net.wanderinghorse.jsonmessage.response."+key;
                try { c = cl.loadClass(key); }
                catch( ClassNotFoundException cnf2 ) { /*ignore*/ }
            }
            /** FIXME: do not call newInstance() unless we know c is-a Response.
                Otherwise we have a potential DoS here where someone intentionally
                instantiates non-response types which have high instantiation costs.
            */
            return ( null == c ) ? null : (Response)c.newInstance();
        }
        catch( Exception ex ) {
            return convertExceptionsToErrorResponse
                    ? new ErrorResponse(ex)
                    : null;
        }
    }

    /**
     * Equivalent to loadRequestHandler( key, true ).
     * @param key The lookup kee.
     * @return The loaded response object or null.
     * @throws RuntimeException See loadRequestHandler(String,boolean).
     */
    Response loadResponse( String key ) {
        return this.loadResponse( key, true );
    }
    /**
     * Equivalent to loadRequestHandler( r.getType(), convertExceptions ).
     * @param r The request to search for a response for.
     * @param convertExceptions See loadRequestHandler(String,boolean).
     * @return See loadRequestHandler(String,boolean).
     * @throws RuntimeException See loadRequestHandler(String,boolean).
     */
    Response loadResponse( Request r, boolean convertExceptions ) {
        return this.loadResponse( r.getType(), convertExceptions );
    }
    
    /**
     * Equivalent to loadRequestHandler( req.getType() ).
     *
     * This function does not actually process the request - it only loads
     * the response handler for it.
     *
     * @param req The request to find a response for.
     * @return The Response object, or null if none
     * was found.
     * @throws NullPointerException if req is null.
     */
    Response loadResponse( final Request req ) {
        return this.loadResponse( req.getType() );
    }
}
